#!/usr/bin/python

"""
    This file is part of benjap.

    Copyright Graham Jones 2010.

    Benjap is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    banjap is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with benjap (COPYING file).  
    If not, see <http://www.gnu.org/licenses/>.

    $Id$

bounce application
Based on the nice demonstration by Ron Dippold 
(http://www.pygame.org/project-Bouncy+Ball+tank-864-.html)

"""
import os    

# Start pygame
from appBase import appBase
import pygame
from pygame.locals import *
#from pygame.locals import QUIT, KEYDOWN, \
#    K_ESCAPE, MOUSEBUTTONDOWN, MOUSEMOTION, K_q, K_F4
import random
import math

# Collision 'friction' factor. We use ELASTIC for everything except
# ELASTIC_FLOOR for a Ball bouncing off the horizontal part of a Wall.
# I told you this wasn't real physics!
ELASTIC = 1.0
ELASTIC2 = 1.0
ELASTIC_FLOOR = 1.0
COLLIDE_FUZZY = 2



# -----------------------------------------------------------------------------
#
# class Wall
#
# -----------------------------------------------------------------------------

class Wall(pygame.sprite.Sprite):
	"""
	A Wall is just a nonmoving rectangle (but still a sprite) that acts as an
	obstacle. It can be an obstacle block right in the middle of the tank or
	one of the restraining walls on the left, right, and bottom.

	All walls are orthoganal since Pygame sprite collision only handles
	rectangles - something at a 45 degree angle would match a collision for
	the entire area from _xy1 to _xy2 even though 90% of it was empty. I
	could do arbitrary plane collisions, but that's not the point of this.
	"""
	def __init__( self, _xy1, _xy2, _color=[255,0,0] ):
		"""
		_xy1 is the upper left corner [x,y]
		_xy2 is the lower right corner [x,y]
		_color is an optional rgb triplet [r,g,b]
		"""
		pygame.sprite.Sprite.__init__(self)
		self.image = pygame.Surface( 
                    [_xy2[0]-_xy1[0]+1, _xy2[1]-_xy1[1]+1 ] )
		self.image.fill(_color)

		self.rect = self.image.get_rect()
		self.rect.topleft = _xy1;

		# just in case
		self.id = 0

	def do_hit( self, ball ):
		"""
		The logic for a ball colliding with us - the ball bounces, 
                we don't!
		Notice that we actually do change the trajectory of the ball 
                that collided with us, which may seem a little strange.
		"""

		# Get our starting speeds
		speedx = ball.speed[0] * ELASTIC
		speedy = ball.speed[1] * ELASTIC_FLOOR

		# Bounce cleanly off a 'floor' - no change in horizontal speed
		if self.rect.left < ball.rect.centerx < self.rect.right:
			vbounce = ball.rect.centery - self.rect.centery
			hbounce = 0

		# Bounce cleanly off a 'wall' - no change in vertical speed
		elif self.rect.top < ball.rect.centery < self.rect.bottom:
			vbounce = 0
			hbounce = ball.rect.centerx - self.rect.centerx
			
		# Otherwise, bounce off the corner
		else:
			vbounce = ball.rect.centery - self.rect.centery
			hbounce = ball.rect.centerx - self.rect.centerx

		# Do the actual speed modifications depending on which side
		# we hit on.
		if vbounce>0:
			speedy = abs(speedy)
		elif vbounce<0:
			speedy = -abs(speedy)
		if hbounce>0:
			speedx = abs(speedx)
		elif hbounce<0:
			speedx = -abs(speedx)

		# And finally assign it
		ball.speed = [ speedx, speedy ]
		

# -----------------------------------------------------------------------------
#
# class Ball
#
# -----------------------------------------------------------------------------

class Ball(pygame.sprite.Sprite):
    """
    A Ball is well, a ball. It has a size (though we don't care about that,
    only Pygame does), mass, and color.
    """

    # ----------------------------
    # Class 'static' variables
    # ----------------------------
	
    # Preload all our bitmap files
    BALLFILES = ( "images/ball16x16.gif", 
              "images/ball12x12.gif", 
              "images/ball8x8.gif",
              "images/smile12x12.gif" )
    BALLFILES = ( "images/bubble.png",)

    types = {}
    for f in BALLFILES:
        image = pygame.image.load(f)
        radius = (image.get_width()+image.get_height())/2.0
        types[f] = {
            # file name just for debugging
            "file" : 	f,
            # the loaded image - do not convert yet, we need palette!
            "image" : 	image,
            # Mass is actually 4/3 * pi * r^3, but we just care about ratio
            "mass"  :   pow(radius,3),
            # Cached colorized versions - none yet
            "colors" :  {}
            }

    BRIGHT_COLORS = ( (0,0,255), (0,255,0), 
		  (0,255,255), (255,0,0), 
		  (255,0,255), (255,255,0), 
		  (255,255,255) )

    # Don't spit out all sorts of stuff about collisions
    verbose_collision = False

    # Keep track of ball IDs for debugging
    next_id = 1

    # How many active balls
    active_count = 0

    # --------------------------
    # Construct a new Ball
    # --------------------------
    def __init__( self, _type, _color, _pos, _speed ):
        """
        type is the ball type name from BALLFILES
        color is (r,g,b) - a (tuple), not a [list]
        startpos is starting [x,y]
        startspeed is starting [xspeed,yspeed] in pixels/second
        """
        pygame.sprite.Sprite.__init__( self )
        type = Ball.types[_type]
        #print "create ball - type=", type
        if not type["colors"].get(_color):
            image = type["image"]
            #image.set_palette_at( 0, _color )

            # Now set the image color key, and RLE accelerate it
            image.set_colorkey( image.get_at((0,0)), RLEACCEL )
            # Save it in the cache map
	    image = pygame.transform.smoothscale(image, 
                                                  (50,50)).convert_alpha()
            type["colors"][_color] = image.convert_alpha()

        # Our ball image is just the image in the color cache
        self.image = type["colors"][_color]

        # our own id - just for debugging purposes
        self.id = Ball.next_id
        Ball.next_id += 1

        # One more ball is active
        Ball.active_count += 1

        # Mass for collisions
        self.mass = type["mass"]

        # Get our rectangle and set position
        self.rect = self.image.get_rect()
        self.rect.topleft = _pos

        self.pos = [ _pos[0], _pos[1] ]
        self.speed = _speed

        # Start us off with an empty list of things not to collide with - see COLLIDE_FUZZY
        self.dont_collide = {}


    # override the kill so we can decrement the number of active balls
    def kill( self ):
        Ball.active_count -= 1
        pygame.sprite.Sprite.kill( self )
		
    #
    # Move a Ball depending on how much time has passed
    #
    def update( self, delay ):
        """
        delay is in miliseconds
        """
        self.pos[0] += self.speed[0] * delay/1000.
        self.pos[1] += self.speed[1] * delay/1000.
        self.rect.topleft = self.pos

    #
    # check to see if the ball is out or has 'stopped' on the bottom
    #
    def out( self ):
        """
        Return True if the ball is out of the tank.
        """
        return self.pos[1]>WINDOWY or self.pos[0]<0 or self.pos[0]>WINDOWX

    def stopped( self ):
        """
        Return True if the ball is stopped (or close enough) on the bottom.
        """
        return self.pos[1]>WINDOWY or \
            ( self.pos[1]>=(WINDOWY-WALL_WIDTH-self.rect.height-1 ) and 
              abs(self.speed[0])<0.5 and abs(self.speed[1])<0.5 )

    #
    # Hit another ball
    #
    def do_hit( self, ball ):
        """
        The logic for another ball hitting us. We only modify the speed for
        BOTH balls (might as well only do the calculation once), then skip
        us hitting them separately.
           See http://www.plasmaphysics.org.uk/collision2d.htm 
        """

        # Difference in ball speeds
        deltavx = ball.speed[0]- self.speed[0]
        deltavy = ball.speed[1]- self.speed[1]

        # velocity difference angle - note this is ball minus self
        gammav = math.atan2( deltavy, deltavx )

        # position difference angle - note this is self minus ball
        gammaxy = math.atan2( self.rect.centery-ball.rect.centery,
                              self.rect.centerx-ball.rect.centerx )

        # relative angle of impact
        # alpha = math.asin(math.sin(gammaxy-gammav))
        alpha = gammaxy-gammav

        # Figure how much is x and how much is y
        # a = math.tan( gammav + alpha )
        a = math.tan( gammaxy )

        # Mass ratio between the balls
        mratio= self.mass/ball.mass

        # Now figure the difference in x speed - then compensate for mass ratio
        deltavx2 = (deltavx + a*deltavy) / (1+a*a)
        deltavx2 *= 2/(1+mratio)
        
        # difference in y speed is just difference in x * a
        deltavy2 = deltavx2 * a

        # and apply the velocity differences
        self.speed[0] = self.speed[0]*ELASTIC2 + deltavx2
        self.speed[1] = self.speed[1]*ELASTIC2 + deltavy2

        ball.speed[0] = ball.speed[0]*ELASTIC2 - deltavx2*mratio
        ball.speed[1] = ball.speed[1]*ELASTIC2 - deltavy2*mratio

        # Don't collide with us again for a while - see COLLIDE_FUZZY docs
        ball.dont_collide[self] = COLLIDE_FUZZY

        # Ball just collided with us, don't collide with ball
        self.dont_collide[ball] = COLLIDE_FUZZY



class bounceApp(appBase):

    def __init__(self,parent):
    
        print "bounceApp.__init__()"
        appBase.__init__(self,parent)

        self.origin=(int(self.screen_size[0]/2),
                     int(self.screen_size[1]/2))

        self.balls = pygame.sprite.RenderUpdates()
        for i in range(1,10):
            self.balls.add(self.newBall())
        self.Sprites = self.balls

	self.walls = pygame.sprite.RenderPlain()
	self.walls.add( Wall( [0,0], 
                         [5,self.screen_size[1]-1] ) )
	self.walls.add( Wall( [0,self.screen_size[1]-1-5], 
                         [self.screen_size[0]-1,self.screen_size[1]-1] ) )
	self.walls.add( Wall( [self.screen_size[0]-1-5,0], 
                         [self.screen_size[0]-1,self.screen_size[1]-1]	) )
	self.walls.add( Wall( [0,0], 
                         [self.screen_size[0]-1,5]	) )

	# Now move the walls to the background.
	# This wouldn't work if we had any moving walls.
	self.walls.draw( self.screen )
        pygame.display.flip()

        musicFile = "sounds/boing_noise.wav"
        self.boingNoise = pygame.mixer.Sound(musicFile)

    #
    # generate a new ball at a random location
    #
    def newBall(self):
	"""
	Generate a new ball at a semi-random location at the top of the tank.
	Speed is also semi-random, color is random
        
	Note - this can return a ball falls out of the window! No big deal, we
        just kill it when it falls below the bottom (off the screen)
        
	"""
	return Ball(
		_type =		random.choice(Ball.BALLFILES),
		_color = 	random.choice(Ball.BRIGHT_COLORS),
		_pos =		[ (self.screen_size[0]-20)*random.random()+10, 
                                  (self.screen_size[1]-20)*random.random()+10 ],
		_speed =	[ random.random()*160-80, random.random()*160-80 ]
		)





    def processEvent(self,event):
        #print "starsApp.processEvent()"
        #if event.type == MOUSEBUTTONDOWN:
        #    if event.button == 1:
        #        self.setOrigin(event.pos)
        pass

    def processDisplay(self,delay):
        #self.balls.add(self.newBall())

        for ball in self.balls:
            ball.update(delay)
        self.collide(self.balls,self.walls)
        self.collide(self.balls,self.balls)

    #
    # Run collision of one group with another
    #
    def collide( self, balls, obstacles ):
	"""
	Run results of collision of one group of sprites with another.
	"""

        # Let the engine find the collisions
	hits = pygame.sprite.groupcollide( balls, obstacles, False, False )

	# Now loop through the collision sets which is a 
        # dictionary of ball/list of hit items
	for ball, hititems in hits.iteritems():
            # We know 'ball' collided with 'hititems' - so for every hititem...
            for hititem in hititems:
                # Don't collide with ourself
                if hititem is ball: 
                    continue
                # don't collide with x if x just collided with us - see COLLIDE_FUZZY
                if hititem in ball.dont_collide:
                    continue

                # Calculate the hit effect
                #print "hititem=",hititem
		self.boingNoise.play()
                hititem.do_hit( ball )



if __name__ == "__main__":
    ba = bounceApp(None)
    ba.mainLoop()
